/*
 * File     : StructureFieldFactory.java
 * Created  : 10 Jan 2012
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal.structure;

import static com.googlecode.dni.internal.DniInternal.LOGGER;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.Buffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;

import com.googlecode.dni.ByValue;
import com.googlecode.dni.internal.string.FastCharset;
import com.googlecode.dni.internal.structure.StructureField.StructureFieldMap;
import com.googlecode.dni.type.CustomNativeObject;
import com.googlecode.dni.type.NativeObjectList;
import com.googlecode.dni.type.NativeStringBuffer;
import com.googlecode.dni.type.Pointer;
import com.googlecode.dni.type.structure.FieldIndex;
import com.googlecode.dni.type.structure.FieldOffset;
import com.googlecode.dni.type.structure.IllegalStructureException;
import com.googlecode.dni.type.structure.Structure;

/**
 * Generates structure field code.
 *
 * @author Matthew Wilson
 */
public final class StructureFieldFactory
{

    private final ClassWriter classWriter;

    private final String implName;

    private final FastCharset fastCharset;

    private final FieldPointerProvider fieldPointerProvider;

    private final Map< String, StructureField > fieldMap = new HashMap< String, StructureField >();

    private Collection< StructureField > orderedFields = this.fieldMap.values();

    private final List< Method > unusedMethods = new ArrayList< Method >();

    private final StructureFieldMap structureFieldMap = new StructureFieldMap()
    {
        @Override
        public StructureField getField( final String name )
        {
            return StructureFieldFactory.this.fieldMap.get( name );
        }
    };

    private final StructureManager.Session session;


    /**
     * Creates a new instance.
     *
     * @param classWriter
     *            the class writer
     * @param implName
     *            the implementation type name
     * @param fastCharset
     *            the fast character set
     * @param fieldPointerProvider
     *            provides absolute pointers to the fields' locations;
     *            non-<code>null</code> for structure fields on a library
     */
    public StructureFieldFactory( final ClassWriter classWriter,
                                  final String implName,
                                  final FastCharset fastCharset,
                                  final FieldPointerProvider fieldPointerProvider )
    {
        assert fieldPointerProvider != null;

        this.classWriter = classWriter;
        this.implName = implName;
        this.fastCharset = fastCharset;
        this.fieldPointerProvider = fieldPointerProvider;
        this.session = StructureManager.NO_SESSION;
    }

    /**
     * Creates a new instance.
     *
     * @param classWriter
     *            the class writer
     * @param implName
     *            the implementation type name
     * @param fastCharset
     *            the fast character set
     * @param session
     *            the owning session
     */
    StructureFieldFactory( final ClassWriter classWriter,
                           final String implName,
                           final FastCharset fastCharset,
                           final StructureManager.Session session )
    {
        this.classWriter = classWriter;
        this.implName = implName;
        this.fastCharset = fastCharset;
        this.session = session;
        this.fieldPointerProvider = null;
    }

    /**
     * Adds a method to be considered.
     *
     * @param method
     *            the method
     */
    public void addMethod( final Method method )
    {
        // 1) look for getters
        boolean isGetter = method.getName().startsWith( "get" );
        boolean isUpdate = method.getName().startsWith( "update" );
        if ( isGetter || isUpdate )
        {
            addGetter( method, isGetter, isUpdate );
        }
        else
        {
            // save for second pass
            this.unusedMethods.add( method );
        }
    }

    /**
     * Notifies the factory that all methods have now been added.
     */
    public void completedMethods()
    {
        // 2) find equivalent setters
        findFieldSetters();

        if ( !this.unusedMethods.isEmpty() )
        {
            throw new IllegalStructureException( "Unrecognised extra method on structure: " + this.unusedMethods.get( 0 ) );
        }

        // 3) check all fields are complete
        checkSetters();
    }

    /**
     * Writes all the methods
     */
    public void writeMethods()
    {
        Map< String, StructureField > createdFields = new LinkedHashMap< String, StructureField >();
        for ( StructureField field : this.fieldMap.values() )
        {
            field.allocateFields( this.classWriter, this.implName, createdFields );
        }

        for ( StructureField field : this.orderedFields )
        {
            field.writeMethods( this.classWriter, this.implName );
        }
    }

    /**
     * Writes the fragment of a static initialiser.
     *
     * @param methodVisitor
     *            the method visitor
     */
    public void writeStaticInitialiserFragment( final MethodVisitor methodVisitor )
    {
        for ( StructureField field : this.orderedFields )
        {
            field.writeStaticInitialiserFragment( methodVisitor, this.implName );
        }
    }

    /**
     * Writes the fragment of a constructor.  Must be used in each constructor.
     *
     * @param methodVisitor
     *            the method visitor
     */
    public void writeConstructorFragment( final MethodVisitor methodVisitor )
    {
        for ( StructureField field : this.orderedFields )
        {
            field.writeConstructorFragment( methodVisitor, this.implName );
        }
    }

    /**
     * @return the class writer
     */
    ClassWriter getClassWriter()
    {
        return this.classWriter;
    }

    /**
     * Sorts the fields.  If this isn't called, the fields are unordered.
     *
     * @param comparator
     *            the comparator
     */
    void sortFields( final Comparator< StructureField > comparator )
    {
        List< StructureField > list = new ArrayList< StructureField >( this.fieldMap.values() );
        this.orderedFields = list;
        Collections.sort( list, comparator );

        if ( LOGGER.isLoggable( Level.FINE ) )
        {
            for ( final StructureField field : this.orderedFields )
            {
                LOGGER.fine( " * " + field );
            }
        }
    }

    /**
     * @return the ordered fields
     */
    Collection< StructureField > getOrderedFields()
    {
        return this.orderedFields;
    }

    private void addGetter( final Method method,
                            final boolean isGetter,
                            final boolean isUpdate )
    {
        Class< ? >[] parameterTypes = method.getParameterTypes();
        Class< ? > returnType = method.getReturnType();
        Type genericReturnType = method.getGenericReturnType();

        checkGetterOrUpdate( method, isGetter, isUpdate, parameterTypes, returnType );

        StructureField field = null;
        String name;

        if ( isGetter )
        {
            name = method.getName().substring( 3 );

            boolean byValue = method.getAnnotation( ByValue.class ) != null;

            if ( returnType.isPrimitive() )
            {
                field = new PrimitiveStructureField();
            }
            else if ( returnType == Pointer.class )
            {
                field = new PointerStructureField();
            }
            else if ( returnType.isInterface() && Structure.class.isAssignableFrom( returnType ) )
            {
                if ( byValue )
                {
                    field = new StructureByValueStructureField();
                }
                else
                {
                    field = new StructureByReferenceStructureField();
                }
            }
            else if ( ( returnType == List.class || returnType == NativeObjectList.class )
                      && genericReturnType instanceof ParameterizedType )
            {
                field = createArrayField( method, (ParameterizedType) genericReturnType, byValue );
            }
            else if ( returnType == NativeStringBuffer.class )
            {
                field = new NativeStringBufferStructureField();
            }
            else if ( returnType.isEnum() )
            {
                field = new EnumStructureField();
            }
            else if ( Buffer.class.isAssignableFrom( returnType ) )
            {
                field = new SizedBufferStructureField();
            }
            else if ( CustomNativeObject.class.isAssignableFrom( returnType ) )
            {
                field = new CustomNativeObjectStructureField();
            }

            if ( field == null )
            {
                throw new IllegalArgumentException( "Structure field unsupported type: " + method );
            }
        }
        else
        {
            assert isUpdate;
            name = method.getName().substring( 6 );
            field = new BufferStructureField();
        }

        field.setSession( this.session );
        field.setGetter( method );
        field.setName( name );

        FieldIndex fieldIndex = method.getAnnotation( FieldIndex.class );
        field.setIndex( fieldIndex != null ? fieldIndex.value() : this.fieldMap.size() );

        FieldOffset fieldOffset = method.getAnnotation( FieldOffset.class );
        if ( fieldOffset != null )
        {
            field.setOffset( fieldOffset.value() );
        }

        field.setFastCharset( this.fastCharset );
        field.setFieldMap( this.structureFieldMap );

        if ( this.fieldPointerProvider != null )
        {
            Pointer absolutePointer = this.fieldPointerProvider.getFieldPointer( field.getGetter(), field.getName() );
            if ( absolutePointer == null )
            {
                throw new IllegalArgumentException( "Missing symbol: " + field.getName() );
            }

            field.setAbsolutePointer( absolutePointer );
        }

        Object duplicate = this.fieldMap.put( field.getName(), field );
        if ( duplicate != null )
        {
            throw new IllegalArgumentException( "Duplicate field name: " + method.getDeclaringClass().getName()
                                                + ": " + field.getName() );
        }
    }

    private static StructureField createArrayField( final Method method,
                                                    final ParameterizedType genericReturnType,
                                                    final boolean byValue )
    {
        StructureField field = null;

        ParameterizedType parameterizedType = genericReturnType;
        Type elementType = parameterizedType.getActualTypeArguments()[ 0 ];
        if ( elementType instanceof Class< ? > )
        {
            Class< ? > elementClass = (Class< ? >) elementType;
            if ( elementClass.isInterface() && Structure.class.isAssignableFrom( elementClass ) )
            {
                if ( !byValue )
                {
                    throw new IllegalStructureException( "List<Structure> must be @ByValue: " + method );
                }
                field = new StructureArrayStructureField();
            }
        }
        return field;
    }

    private static void checkGetterOrUpdate( final Method method,
                                             final boolean isGetter,
                                             final boolean isUpdate,
                                             final Class< ? >[] parameterTypes,
                                             final Class< ? > returnType )
    {
        if ( isGetter && parameterTypes.length > 0 )
        {
            throw new IllegalStructureException( "Structure getter must take no parameters: " + method );
        }

        if ( isUpdate )
        {
            if ( parameterTypes.length != 1 || !Buffer.class.isAssignableFrom( parameterTypes[ 0 ] ) )
            {
                throw new IllegalStructureException( "Structure updater must take a Buffer parameter: " + method );
            }

            if ( returnType != boolean.class )
            {
                throw new IllegalStructureException( "Structure updater must return a boolean: " + method );
            }

            if ( method.getAnnotation( ByValue.class ) != null )
            {
                throw new IllegalStructureException( "Structure updater cannot be @ByValue: " + method );
            }
        }
    }

    private void findFieldSetters()
    {
        for ( Iterator< Method > iterator = this.unusedMethods.iterator(); iterator.hasNext(); )
        {
            Method method = iterator.next();
            if ( method.getName().startsWith( "set" ) )
            {
                String name = method.getName().substring( 3 );
                StructureField field = this.fieldMap.get( name );
                if ( field == null )
                {
                    continue;
                }

                if ( method.getReturnType() != void.class )
                {
                    throw new IllegalArgumentException( "Structure setter must return void: " + method );
                }

                field.setSetter( method );

                iterator.remove();
            }
        }
    }

    private void checkSetters()
    {
        for ( StructureField field : this.fieldMap.values() )
        {
            if ( !field.expectsSetter() && field.getSetter() != null )
            {
                throw new IllegalArgumentException( "Unexpected setter: " + field.getSetter() );
            }
        }
    }

    /**
     * Provides absolute pointers to fields' locations.
     *
     * @author Matthew Wilson
     */
    public interface FieldPointerProvider
    {
        /**
         * Determines the absolute pointer to the field's location.
         *
         * @param getter
         *            the getter method
         * @param fieldName
         *            the field's name
         * @return the pointer
         */
        Pointer getFieldPointer( Method getter, String fieldName );
    }

}
